﻿///////////////////////////////////////////////////////////////////////////////////////////////
// System.Text.Parsing - © 2011 Sebastien Pellizzari
// Released under the MIT License (http://www.opensource.org/licenses/mit-license.php)
///////////////////////////////////////////////////////////////////////////////////////////////
using System.CodeDom.Compiler;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace System.Text.Parsing
{
    /// <summary>Represents a text tokenizer that uses regex to produce tokens.</summary>
    /// <typeparam name="TTokenValue">The type of token value.</typeparam>
    public abstract class RegexTextTokenizer<TTokenValue, TCallback> : ITokenizer<TextRange, TTokenValue>
        where TCallback : RegexTextTokenizer<TTokenValue, TCallback>.ICallback
    {
        private int _currentIndex;              // Current location.
        private int _currentLineNumber;         // Current location.
        private int _currentColumnNumber;       // Current location.
        private int _firstChar;                 // First index in the string.
        private int _lastChar;                  // Last index in the string.
        private string _sourceData;             // The source string.
        private TCallback _callback;            // Callback object.
        private TokenDefinition[] _tokenTable;  // Token table.

        //-----------------------------------------------------------------------
        //     Properties
        //-----------------------------------------------------------------------

        /// <summary>Gets the token table.</summary>
        public TokenDefinition[] TokenTable { get { return _tokenTable; } }

        //-----------------------------------------------------------------------
        //     Constructors
        //-----------------------------------------------------------------------

        /// <summary>Initializes an instance of the <see cref="RegexTextTokenizer"/> class.</summary>
        protected RegexTextTokenizer()
        {
        }

        //-----------------------------------------------------------------------
        //     Methods
        //-----------------------------------------------------------------------

        /// <summary>Initializes the tokenizer.</summary>
        /// <param name="tokenTable">The token table.</param>
        /// <param name="callback">The callback object.</param>
        protected void Initialize(TokenDefinition[] tokenTable, TCallback callback)
        {
            if (tokenTable == null) throw new ArgumentNullException("tokenTable");
            if (callback == null) throw new ArgumentNullException("callback");
            _callback = callback;
            _tokenTable = tokenTable;
        }

        /// <summary>Sets the initial location of produced tokens.</summary>
        /// <param name="location">A <see cref="TextLocation"/> value.</param>
        public void SetStartLocation(TextLocation location)
        {
            _currentLineNumber = location.LineNumber;
            _currentColumnNumber = location.ColumnNumber;
        }

        /// <summary>Sets the source data of the tokenizer.</summary>
        /// <param name="source">The source string.</param>
        /// <param name="offset">Offset in the source string.</param>
        /// <param name="count">The number of characters to use.</param>
        public void SetSourceData(string source, int offset, int count)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (offset < 0 || offset >= source.Length) throw new ArgumentOutOfRangeException("offset");
            if (count < 0 || offset + count > source.Length) throw new ArgumentOutOfRangeException("count");
            _sourceData = source;
            _firstChar = offset;
            _lastChar = offset + count;
            _currentIndex = _firstChar;
        }

        /// <summary>Returns the next token from the source data.</summary>
        /// <param name="context">The scanning context.</param>
        /// <param name="tokenTypeId">The ID of scanned token type.</param>
        /// <param name="tokenOptions">Options of scanned token.</param>
        /// <param name="tokenRange">The source data range of scanned token.</param>
        /// <param name="tokenValue">Storage for computed token value.</param>
        /// <typeparam name="TContext">The type of scanner context.</typeparam>
        public void Scan<TContext>(TContext context, out int tokenTypeId, out TokenOptions tokenOptions, out TextRange tokenRange, ref TTokenValue tokenValue)
            where TContext: ITokenizerContext
        {
            if (_sourceData == null) throw new InvalidOperationException("No source data");

            // Compute start location.
            var startLocation = new TextLocation(_currentIndex, _currentLineNumber, _currentColumnNumber);

            // If we reached the end of the input...
            if (_currentIndex >= _lastChar)
            {
                // Return EOF.
                tokenRange = new TextRange(ref startLocation, ref startLocation);
                tokenTypeId = TokenTypeIds.EOF;
                tokenOptions = TokenOptions.None;
                return;
            }

            // For each token type...
            var longestMatchLength = -1;
            var longestMatchIndex = -1;
            var longestMatch = (Match)null;
            for (int i = 0; i < _tokenTable.Length; i++)
            {
                // If this type of token is not allowed, skip it.
                if (!context.IsExpected(TokenTypeIds.User + i) && !_tokenTable[i].Ignore) continue;

                // Try to match someting.
                var match = _tokenTable[i].Regex.Match(_sourceData, _currentIndex, _lastChar - _currentIndex);

                // If no match or zero-length, continue.
                if (!match.Success || match.Length == 0) continue;

                // If it's the longest match we found...
                if (match.Length > longestMatchLength)
                {
                    // If we have a callback, call it to check the token.
                    if (!_callback.ValidateToken(TokenTypeIds.User + i, match)) continue;

                    // Keep it.
                    longestMatch = match;
                    longestMatchIndex = i;
                    longestMatchLength = match.Length;
                }
            }

            // If we found a match...
            if (longestMatch != null)
            {
                // Update position.
                Advance(longestMatchLength);

                // Compute end location.
                var endLocation = new TextLocation(_currentIndex, _currentLineNumber, _currentColumnNumber);

                // Compute token.
                tokenRange = new TextRange(ref startLocation, ref endLocation);
                tokenTypeId = TokenTypeIds.User + longestMatchIndex;
                tokenOptions = _tokenTable[longestMatchIndex].Ignore ? TokenOptions.Ignore : TokenOptions.None;
                _callback.ComputeTokenValue(tokenTypeId, longestMatch, ref tokenValue);
            }
            else
            {
                // Advance of 1 character.
                Advance(1);

                // Compute end location.
                var endLocation = new TextLocation(_currentIndex, _currentLineNumber, _currentColumnNumber);

                // Compute token.
                tokenRange = new TextRange(ref startLocation, ref endLocation);
                tokenTypeId = TokenTypeIds.Error;
                tokenOptions = TokenOptions.None;
            }
        }

        //-----------------------------------------------------------------------
        //     Non Public
        //-----------------------------------------------------------------------

        // Advance method.
        private void Advance(int count)
        {
            for (int i = 0; i < count; i++)
            {
                if (_sourceData[_currentIndex] == '\n')
                {
                    _currentLineNumber++;
                    _currentColumnNumber = 1;
                }
                else _currentColumnNumber++;
                _currentIndex++;
            }
        }

        //-----------------------------------------------------------------------
        //     Nested Classes
        //-----------------------------------------------------------------------

        /// <summary>Represents a token definition.</summary>
        public struct TokenDefinition
        {
            /// <summary>Determines whether the tokens of that type are to be ignored.</summary>
            public readonly bool Ignore;
            /// <summary>The regular expression matching strings of that token type.</summary>
            public readonly Regex Regex;
            /// <summary>The sequence of token groups enclosing the token definition.</summary>
            public readonly string Groups;

            //-----------------------------------------------------------------------
            //     Constructors
            //-----------------------------------------------------------------------

            /// <summary>Initializes a value of the <see cref="TokenDefinition"/> type.</summary>
            public TokenDefinition(Regex regex, bool ignore, string groups)
            {
                if (regex == null) throw new ArgumentNullException("regex");
                Regex = regex;
                Ignore = ignore;
                Groups = groups;
            }
        }

        /// <summary>Represents an interface for a callback object used by the tokenizer during processing.</summary>
        public interface ICallback
        {
            /// <summary>Determines whether specified match is valid for specified token type.</summary>
            /// <param name="tokenTypeId">The ID of the token type.</param>
            /// <param name="match">The regex match to check.</param>
            /// <returns>true if valid; false otherwise.</returns>
            bool ValidateToken(int tokenTypeId, Match match);

            /// <summary>Computes the value of a token from specified match and token type.</summary>
            /// <param name="tokenTypeId">The ID of the token type.</param>
            /// <param name="match">The regex match.</param>
            /// <param name="value">Storage for the value to compute.</param>
            void ComputeTokenValue(int tokenTypeId, Match match, ref TTokenValue value);
        }
    }
}
